package cn.ric.mycurd;

import cn.ric.mycurd.annotations.MycurdDataSource;
import cn.ric.mycurd.annotations.MycurdDataSources;
import cn.ric.mycurd.beanconfigs.MycurdDataSourceFactory;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScannerRegistrar;
import org.mybatis.spring.mapper.MapperScannerConfigurer;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.GenericBeanDefinition;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.context.EnvironmentAware;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.annotation.MergedAnnotation;
import org.springframework.core.annotation.MergedAnnotations;
import org.springframework.core.env.Environment;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.FileUrlResource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PropertiesLoaderUtils;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.util.StringUtils;

import javax.sql.DataSource;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.Map;
import java.util.Properties;

/**
 * @author ric.zhang
 */
@Import(MapperScannerRegistrar.class)
public class MycurdBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar, EnvironmentAware {
    private Environment environment;
    private static final String PLACEHOLDER_REGEX = "^\\$\\{.*}$";

    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        final MergedAnnotations annotations = importingClassMetadata.getAnnotations();
        if (annotations.isPresent(MycurdDataSources.class)) {
            final MergedAnnotation<MycurdDataSources> mycurdDataSources = annotations.get(MycurdDataSources.class);
            final MergedAnnotation<MycurdDataSource>[] values =
                mycurdDataSources.getAnnotationArray("value", MycurdDataSource.class);
            for (int index = 0; index < values.length; index++) {
                MergedAnnotation<MycurdDataSource> mycurdDataSource = values[index];
                initMycurdDataSource(registry, mycurdDataSource, index);
            }
        } else if (annotations.isPresent(MycurdDataSource.class)) {
            initMycurdDataSource(registry, annotations.get(MycurdDataSource.class), 0);
        }
        // 释放变量
        this.environment = null;
    }

    private void initMycurdDataSource(BeanDefinitionRegistry registry,
        MergedAnnotation<MycurdDataSource> mycurdDataSource, int index) {
        String jdbcConfigLocation = mycurdDataSource.getString("jdbcConfigLocation");
        if (jdbcConfigLocation.matches(PLACEHOLDER_REGEX)) {
            jdbcConfigLocation = environment.resolvePlaceholders(jdbcConfigLocation);
        }
        LoggerFactory.getLogger(MycurdBeanDefinitionRegistrar.class).info("registering jdbc: {}", jdbcConfigLocation);
        final String id = mycurdDataSource.getString("id");
        final String suf = StringUtils.hasText(id) ? id : String.valueOf(index + 1);
        Properties allProperties;
        Resource resource;
        try {
            // 获取注入的属性
            // 这些属性一部分是直接注入 DataSourceProperties，另一部分需要注入第三方连接池的 DataSource 实例
            if (jdbcConfigLocation.startsWith("classpath:")) {
                final String resourceName = jdbcConfigLocation.substring(10);
                resource = new ClassPathResource(resourceName);
            } else {
                resource = new FileUrlResource(jdbcConfigLocation);
            }
            allProperties = PropertiesLoaderUtils.loadProperties(resource);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }

        // 注册产生 DataSource 的工厂类
        final GenericBeanDefinition dataSourceFactoryDefinition =
            getFactoryBeanDefinition(mycurdDataSource, index, allProperties);
        dataSourceFactoryDefinition.setResource(resource);
        registry.registerBeanDefinition("factory_" + suf, dataSourceFactoryDefinition);

        // 注册 DataSource，由工厂产生
        final GenericBeanDefinition dataSourceDefinition = getDataSourceBeanDefinition(index, suf, allProperties);
        registry.registerBeanDefinition("dataSource_" + suf, dataSourceDefinition);

        // 注册 DataSourceTransactionManager，由工厂产生
        final GenericBeanDefinition transactionManagerBeanDefinition = getTransactionDefinition(index, suf);
        registry.registerBeanDefinition("transactionManager_" + suf, transactionManagerBeanDefinition);

        // 注册 SqlSessionTemplate，由工厂产生
        final GenericBeanDefinition sqlSessionBeanDefinition = getSqlSessionTemplateDefinition(index, suf);
        registry.registerBeanDefinition("sqlSessionTemplate_" + suf, sqlSessionBeanDefinition);

        // 扫描 Mapper
        final BeanDefinitionBuilder builder = getMapperDefinitionBuilder(mycurdDataSource, suf);
        registry.registerBeanDefinition("mybatisMapperClass_" + suf, builder.getBeanDefinition());
    }

    private GenericBeanDefinition getTransactionDefinition(int primary, String suf) {
        final GenericBeanDefinition transactionManagerBeanDefinition = new GenericBeanDefinition();
        transactionManagerBeanDefinition.setPrimary(primary == 0);
        transactionManagerBeanDefinition.setBeanClass(DataSourceTransactionManager.class);
        transactionManagerBeanDefinition.setFactoryBeanName("factory_" + suf);
        transactionManagerBeanDefinition.setFactoryMethodName("createTransactionManager");
        return transactionManagerBeanDefinition;
    }

    private BeanDefinitionBuilder getMapperDefinitionBuilder(MergedAnnotation<MycurdDataSource> mycurdDataSource,
        String suf) {
        String[] basePackages = mycurdDataSource.getStringArray("mapperBasePackages");
        for (int index = 0; index < basePackages.length; index++) {
            String basePackage = basePackages[index];
            if (basePackage.matches(PLACEHOLDER_REGEX)) {
                basePackages[index] = environment.resolvePlaceholders(basePackage);
            }
        }
        final BeanDefinitionBuilder builder =
            BeanDefinitionBuilder.genericBeanDefinition(MapperScannerConfigurer.class);
        builder.addPropertyValue("processPropertyPlaceHolders", true);
        builder.addPropertyValue("sqlSessionTemplateBeanName", "sqlSessionTemplate_" + suf);
        builder.addPropertyValue("basePackage", StringUtils.arrayToCommaDelimitedString(basePackages));
        return builder;
    }

    private GenericBeanDefinition getSqlSessionTemplateDefinition(int primary, String suf) {
        final GenericBeanDefinition sqlSessionBeanDefinition = new GenericBeanDefinition();
        sqlSessionBeanDefinition.setPrimary(primary == 0);
        sqlSessionBeanDefinition.setBeanClass(SqlSessionTemplate.class);
        sqlSessionBeanDefinition.setFactoryBeanName("factory_" + suf);
        sqlSessionBeanDefinition.setFactoryMethodName("createSqlSessionTemplate");
        return sqlSessionBeanDefinition;
    }

    private GenericBeanDefinition getDataSourceBeanDefinition(int primary, String suf, Properties allProperties) {
        final GenericBeanDefinition dataSourceDefinition = new GenericBeanDefinition();
        dataSourceDefinition.setPrimary(primary == 0);
        dataSourceDefinition.setBeanClass(DataSource.class);
        for (Map.Entry<Object, Object> entry : allProperties.entrySet()) {
            dataSourceDefinition.getPropertyValues().add(entry.getKey().toString(), entry.getValue());
        }
        dataSourceDefinition.setFactoryBeanName("factory_" + suf);
        dataSourceDefinition.setFactoryMethodName("createDataSource");
        return dataSourceDefinition;
    }

    private GenericBeanDefinition getFactoryBeanDefinition(MergedAnnotation<MycurdDataSource> mycurdDataSource,
        int primary, Properties allProperties) {
        final Properties factoryProperties = new Properties();
        for (Field declaredField : DataSourceProperties.class.getDeclaredFields()) {
            final String fieldName = declaredField.getName();
            if (allProperties.containsKey(fieldName)) {
                factoryProperties.setProperty(fieldName, allProperties.getProperty(fieldName));
                allProperties.remove(fieldName);
            }
        }
        String mybatisConfigLocation = mycurdDataSource.getString("mybatisConfigLocation");
        if (mybatisConfigLocation.matches(PLACEHOLDER_REGEX)) {
            mybatisConfigLocation = environment.resolvePlaceholders(mybatisConfigLocation);
        }
        String mapperLocations = mycurdDataSource.getString("mapperLocations");
        if (mapperLocations.matches(PLACEHOLDER_REGEX)) {
            mapperLocations = environment.resolvePlaceholders(mapperLocations);
        }
        String typeAliasesPackage = mycurdDataSource.getString("typeAliasesPackage");
        if (typeAliasesPackage.matches(PLACEHOLDER_REGEX)) {
            typeAliasesPackage = environment.resolvePlaceholders(typeAliasesPackage);
        }
        final GenericBeanDefinition dataSourceFactoryDefinition = new GenericBeanDefinition();
        dataSourceFactoryDefinition.setPrimary(primary == 0);
        dataSourceFactoryDefinition.setBeanClass(MycurdDataSourceFactory.class);
        // 是否启用增删改查插件
        dataSourceFactoryDefinition.getPropertyValues().add("enableDynamicSqlInterceptor", environment.resolvePlaceholders("${mycurd.enable:true}"));
        // 注入四个属性值
        allProperties.remove("jdbcConfigLocation");
        dataSourceFactoryDefinition.getPropertyValues().add("mybatisConfigLocation", mybatisConfigLocation);
        allProperties.remove("mybatisConfigLocation");
        dataSourceFactoryDefinition.getPropertyValues().add("mapperLocations", mapperLocations);
        allProperties.remove("mapperLocations");
        dataSourceFactoryDefinition.getPropertyValues().add("typeAliasesPackage", typeAliasesPackage);
        allProperties.remove("typeAliasesPackage");
        for (Map.Entry<Object, Object> entry : factoryProperties.entrySet()) {
            dataSourceFactoryDefinition.getPropertyValues().add(entry.getKey().toString(), entry.getValue());
        }
        return dataSourceFactoryDefinition;
    }

    @Override
    public void setEnvironment(Environment environment) {
        this.environment = environment;
    }
}
